Repository navigation
Add credential fill, vaults, TOTP and 1Password tokens, with LLM tools - #142
Merged
Merged
Conversation
AHarmlessPyro
marked this pull request as ready for review
October 14, 2026 00:46
NikhilShahi
approved these changes
Oct 14, 2026
Credentials are secrets of type "credential" whose value maps field names to strings. The value is checked locally, serialized as compact UTF-8 JSON and sealed with the encryption context the API returns, like any secret. - Secret gains type and metadata (browser.allowedOrigins); create accepts type, metadata and a mapping value; list filters by type; update sends metadata with null kept, so a member can be cleared. - secrets.create_credential() and secrets.set_allowed_origins(). - add_version() checks the value's shape against the secret's type. - sessions.create() takes credentials; the session lists attached ones. - sessions.credentials.fill() validates the request locally and posts it with a timeout of at least 30 seconds. - A str secret value that isn't valid UTF-8 (a lone surrogate) now raises invalid_secret_value instead of UnicodeEncodeError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CredentialFillTool exposes sessions.credentials.fill() to a model with the session ID bound by the application. The model picks the credential by name, the fields and their selectors, and optionally a page ID or an iframe URL; it never sees values. runnable() and async_runnable() return the fill result as compact JSON, or the error's code, message, retryable flag and server details as JSON so the model can react, for example by choosing a page after CREDENTIAL_PAGE_AMBIGUOUS. The definitions come for OpenAI (strict), Anthropic and Gemini. Gemini schemas are an OpenAPI subset, so tools/gemini.py converts the JSON schemas (nullable instead of a null type, no additionalProperties), and every existing tool gains a gemini_tool_definition too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Secret.type is now a str defaulting to "secret", so a new type doesn't break list() or get(). Requests still accept only the known SecretType values. The README's LLM samples use gpt-5, claude-sonnet-5-5 and gemini-3.5-flash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FzcEMZLVjoiueoQFxG6F7S
… the README Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FzcEMZLVjoiueoQFxG6F7S
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FzcEMZLVjoiueoQFxG6F7S
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
The credential secret type is gone: a secret is fillable once it has allowed origins, and a login is usually several secrets grouped in a vault. Secrets take type totp (checked locally as the platform reads seeds) or op_token, and allowed_origins on create. client.vaults manages stored and 1Password vaults; sandboxes and processes take vault next to secrets, and report each binding's source, type and 1Password reference. Sessions attach secrets or vaults, fills take credential or vault with page_url, and fill_credential lets a model choose either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Strip one leading BOM, then Go's whitespace; uppercase per character so ß and ff don't become base32; refuse stray % outside the query, trailing newlines in digits or period, and malformed userinfo; accept userinfo before totp as Go does; read query pairs as Go's ParseQuery. The fill tool answers input that isn't an object with an invalid_request error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
browserctrl #1552 no longer links vaults to 1Password: vaults hold stored secrets only, and 1Password fields are named per request with onePassword on sandbox create and process start. - Vaults: drop source and the 1Password entry, source and token models. Entries are StoredEntry(name, version), reported as VaultEntry, and None in an update removes one. Create and update params refuse unknown members such as a leftover source; a source in a response is ignored. - Sandboxes: one_password=OnePasswordSource(token, vault, fields) on create (image launches), exec and start, sync and async, sent as onePassword. Token and env names, part rules (UTF-16 length, no '/', '?', Cc, Cf, Zl or Zp, not '.' or '..'), field queries, the 512-byte reference, 1-32 fields, strict members, and clashes with secrets or env are all checked before any request. A clash with the vault is left to the server. - Bindings document source type "onePassword" with reference and token_secret. - README: one_password examples and rules, stored-only vaults, and the error codes without VAULT_ENTRY_INVALID and VAULT_SOURCE_INVALID. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Sessions attach an op_token with credentials=[{"one_password": name}],
sent as {"onePassword": name}. sessions.credentials.fill() takes
one_password={"token", "vault", "allowed_origins"} with a ref
{"item", "section"?, "field"} on every field, sent as onePassword /
allowedOrigins, and checks it all locally as #1552 does. Results gain
each field's op:// reference; the top-level version stays optional.
Allowed origins are now checked locally as the server normalizes them,
for secrets (create, update, set_allowed_origins) and for 1Password
fills alike. fill_credential stays without 1Password: a model must not
choose the origins its values may be typed on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
#1573's vm-service drops CR and LF from a seed with its spaces and hyphens before the length check (its base32 decoder skips them), so a seed pasted across lines is valid. The clients refused it; they now drop them too. A raw line break inside an otpauth URI is still refused, as Go's url.Parse does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Allowed origins are trimmed the way JavaScript's trim() does, so a leading byte-order mark is accepted and \x1c-\x1f or \x85 are not, and an IPv6 zone is checked as Node's isIPv6 does, which Python 3.8 refused outright. A `secrets` variable name longer than 256 characters is now refused locally, as the server refuses it. The README now says a frame fill skips the allowed-origin tab step, that SECRET_ONEPASSWORD_AUTH_FAILED also covers a malformed token or one for another sign-in host, and the 503 that CREDENTIAL_FILL_FAILED and SECRET_ONEPASSWORD_FAILED can carry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
The API has sent tab_opened events since browserctrl #559, and the event type Literal refused them, so listing such a session's events failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
AHarmlessPyro
force-pushed
the
credentials
branch
from
October 15, 2026 01:32
f76cd4a to
133d554
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AHarmlessPyro
force-pushed
the
credentials
branch
from
October 15, 2026 18:11
9a0a25a to
cb24670
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds browser credential fill, vaults and typed secrets to the sync and async clients, plus
fill_credentialtool definitions for OpenAI, Anthropic and Gemini. It matches browserctrl master at 08c5d5232: #1572 (fill on plain secrets), #1551 (vaults), #1552 (op_token, per-request 1Password fields and 1Password browser fill) and #1573 (totp). It mirrors node-sdk#131.The
credentialsecret type from #1537 is gone. A login is plain secrets that have allowed origins, usually grouped in a vault (USERNAME,PASSWORD, and optionallyOTPfrom atotpsecret). Values are encrypted locally and typed into pages by the platform, so neither the caller's code nor an LLM ever sees them.API (sync and async; params as dicts or models)
Secrets
client.secrets.create({name, value?, description?, type?: "secret" | "totp" | "op_token", allowed_origins?, metadata?})totp: a base32 seed or anotpauth://totp/URI. The parsing mirrors vm-service'sparseTOTPSeed: spaces, hyphens and line breaks are ignored,=padding is allowed only at the end, and lengths 1, 3 or 6 past a multiple of 8 are refused. In a URI,algorithm,digitsandperiodare range-checked, and HOTP is refused.op_token: a 1Password service account token, named byone_password.token. It takes no allowed origins.add_version(secret, value)checks the value against the secret's stored type.set_allowed_origins(secret, origins | None),update(secret, {description?, metadata?})andlist({type}).Secret.typeis a plain string.create_credentialand object values are removed.Vaults (new
client.vaults, by ID or exact name)list,get,delete.create({name, description?, entries?})andupdate(vault, {description?, entries?: {ENV: StoredEntry | None}}), whereNoneremoves an entry. Vaults hold stored secrets only, as on master; the params are strict, so a leftoversourceor{item, field}entry is refused rather than dropped.Sandboxes
vaulton create (image launches),execandprocesses.start.secretswins on a clash.one_password=OnePasswordSource(token, vault, fields={ENV: OnePasswordField(item, section, field)})(or a dict) on the same calls reads fields of a 1Password vault when the process starts; it's sent asonePassword.name,secret_idandversion, plussecret_type,reference,token_secret,source(vault,secretsoronePassword) andoverrides.startup_secretsacross refreshes.Sessions
credentials: [str | {"secret": ...} | {"vault": ...} | {"one_password": ...}]on create.{"one_password": "op-token"}(sent asonePassword) attaches an op_token so fills can read 1Password fields.client.sessions.credentials.fill(session_id, {credential? | vault?, fields: [{selector, field?}], page_id? | page_url?, frame_url?})version,page_id,originandfields[selector, outcome, secret_id, version, field, expires_at].fill(session_id, {"one_password": {"token", "vault", "allowed_origins"}, "fields": [{"selector", "ref": {"item", "section", "field"}}]}). Field results gainreference, and there's no top-levelversion. Checked locally like node.credential_filledandcredential_fill_refused.Tools:
CredentialFillTool/fill_credential(OpenAI strict, Anthropic, Gemini)credential,vault,fields[{selector, field}],page_id,page_urlandframe_url.Noneand""count as absent, and input that isn't an object returnsinvalid_requestJSON.filldirectly.README: the secrets, vaults and credential fill section is rewritten. It covers
onePassword, error codes and the tool loops.Validation split: the SDK checks request shapes, secret values (UTF-8, size, no NUL) and TOTP seeds locally, since the API can't see an encrypted value. Policy rules (allowed-origin syntax, 1Password names and the 512-byte reference, field and entry limits, source clashes) are left to the API, which returns its status, code,
retryableflag and details unchanged. Seedocs/credentials-cleanup.md.Version
The package is bumped to 1.13.1 (published: 1.12.1).
Validation
mainand the validation cleanup, 1004 tests pass on Python 3.12 (including the mypy and Pyright typing tests) and 1000 on 3.8. Ruff is clean.frame_urltab rule,SECRET_ONEPASSWORD_AUTH_FAILEDfor malformed tokens, and the 503 variants.USERNAME/PASSWORD/OTP, and a session with[{"vault"}];CREDENTIAL_PAGE_AMBIGUOUSwithpage_id, and logged in.parseTOTPSeed(including its line-break fix, 456995310) on 143 edge cases, alongside node-sdk, hx and the dashboard: no differences.one_passwordon sandbox create (the startup command, read throughstartup_secrets),execandprocesses.start; the values matched by hash;Needs browserctrl #1552 and #1573 (both merged) to be deployed for
op_token,one_passwordandtotp.🤖 Generated with Claude Code
https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v